package br.com.afi.util;

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;

/**
 * Essa classe possui métodos utilitários para a manipulação de imagens.
 * 
 * @author André de Fontana Ignacio
 * @version 1.0
 */
public class ImageUtil
{
	/**
	 * Redimensiona a imagem informada. Esse método não modifica a imagem original.
	 * 
	 * @param source Arquivo da Imagem de Origem
	 * @param newWidth Nova largura em pixels
	 * @param newHeight Nova altura em pixels
	 * @param proportional Redimensiona proporcionamente?
	 * @return Nova imagem redimensionada
	 * @throws IOException Lançada quando ocorrer algum problema de leitura ou gravação
	 */
	public static final BufferedImage resize( File source, int newWidth, int newHeight, boolean proportional ) throws IOException
	{
		return resize( ImageIO.read( source ), newWidth, newHeight, proportional );
	}

	/**
	 * Redimensiona a imagem informada. Esse método não modifica a imagem original.
	 * 
	 * @param source Arquivo da Imagem de Origem
	 * @param dest Arquivo de Destino
	 * @param newWidth Nova largura em pixels
	 * @param newHeight Nova altura em pixels
	 * @param proportional Redimensiona proporcionamente?
	 * @throws IOException Lançada quando ocorrer algum problema de leitura ou gravação
	 */
	public static final void resize( File source, File dest, int newWidth, int newHeight, boolean proportional ) throws IOException
	{
		final BufferedImage bi = resize( ImageIO.read( source ), newWidth, newHeight, proportional );
		final String suffix = FileUtil.getExtension( dest );
		final Iterator<ImageWriter> writers = ImageIO.getImageWritersBySuffix( suffix );
		final ImageWriter writer = ( ImageWriter ) writers.next();
		final ImageOutputStream ios = ImageIO.createImageOutputStream( dest );

		writer.setOutput( ios );
		writer.write( bi );
	}

	/**
	 * Redimensiona a imagem informada. Esse método não modifica a imagem original.
	 * 
	 * @param source Imagem de Origem
	 * @param newWidth Nova largura em pixels
	 * @param newHeight Nova altura em pixels
	 * @param proportional Redimensiona proporcionamente?
	 * @return Nova imagem redimensionada
	 * @throws IOException Lançada quando ocorrer algum problema de leitura ou gravação
	 */
	public static final BufferedImage resize( BufferedImage source, int newWidth, int newHeight, boolean proportional ) throws IOException
	{
		// Calculos necessários para manter as propoçoes da imagem, conhecido
		// como "aspect ratio"
		final int sourceWidth = source.getWidth();
		final int sourceHeight = source.getHeight();

		final double sourceRatio = ( double ) sourceWidth / ( double ) sourceHeight;
		final double thumbRatio = ( double ) newWidth / ( double ) newHeight;

		if ( proportional )
		{
			if ( thumbRatio < sourceRatio )
			{
				newHeight = ( int ) ( newWidth / sourceRatio );
			}
			else
			{
				newWidth = ( int ) ( newHeight * sourceRatio );
			}
		}
		final BufferedImage newImage = new BufferedImage( newWidth, newHeight, source.getType() );
		final Graphics2D g = newImage.createGraphics();
		g.setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC );

		final double scaleX = ( double ) newWidth / ( double ) sourceWidth;
		final double scaleY = ( double ) newHeight / ( double ) sourceHeight;

		final AffineTransform transformer = AffineTransform.getScaleInstance( scaleX, scaleY );
		g.drawRenderedImage( source, transformer );
		g.dispose();

		return newImage;
	}

	/**
	 * Redimensiona a imagem informada. Esse método não modifica a imagem original.
	 * 
	 * @param source Arquivo da Imagem de Origem
	 * @param dest Arquivo de Destino
	 * @param newWidth Nova largura em pixels
	 * @param newHeight Nova altura em pixels
	 * @param newQuality Qualidade, valor entre 0 a 1
	 * @param proportional Redimensiona proporcionamente?
	 * @throws IOException Lançada quando ocorrer algum problema de leitura ou gravação
	 */
	public static final void resizeAndChangeQuality( File source, File dest, int newWidth, int newHeight, boolean proportional, float newQuality ) throws IOException
	{
		resize( source, dest, newWidth, newHeight, proportional );
		if ( newQuality != 1 )
		{
			changeQuality( dest, dest, newQuality );
		}
	}

	/**
	 * Modifica a qualidade da imagem informada. Esse método não modifica a imagem original.
	 * 
	 * @param source Arquivo da Imagem de Origem
	 * @param dest Arquivo de Destino
	 * @param newQuality Qualidade da Image, valor entre 0 a 1
	 * @throws IOException Lançada quando ocorrer algum problema de leitura ou gravação
	 */
	public static void changeQuality( File source, File dest, float newQuality ) throws IOException
	{
		writeImage( ImageIO.read( source ), dest, newQuality );
	}

	/**
	 * Grava a imagem no arquivo informado. Esse método não modifica a imagem original.
	 * 
	 * @param sourceImage Imagem de Origem
	 * @param dest Arquivo de destino
	 * @param quality Qualidade da Image, valor entre 0 a 1
	 * @throws IOException Lançada quando ocorrer algum problema de leitura ou gravação
	 */
	public static void writeImage( BufferedImage sourceImage, File dest, float quality ) throws IOException
	{
		final String extension = FileUtil.getExtension( dest );

		if ( extension == null || extension.isEmpty() )
		{
			throw new IllegalArgumentException( "O arquivo de destino deve possuir uma extensão." );
		}

		final String suffix = extension.toLowerCase();
		final ImageOutputStream os = ImageIO.createImageOutputStream( dest );
		final ImageWriter writer = ImageIO.getImageWritersByFormatName( suffix ).next();
		writer.setOutput( os );

		final ImageWriteParam param = writer.getDefaultWriteParam();
		param.setCompressionMode( ImageWriteParam.MODE_EXPLICIT );
		param.setCompressionQuality( quality );
		writer.write( null, new IIOImage( sourceImage, null, null ), param );
	}

	/**
	 * Grava a imagem em disco no caminho informado, com o nome informado. Esse método não modifica
	 * a imagem original.
	 * 
	 * @param image Imagem
	 * @param path Caminho
	 * @param filename Nome do arquivo
	 * @param quality Qualidade da Image, valor entre 0 a 1
	 * @throws IOException Lançada quando não for possível gravar no local informado.
	 */
	public static void writeImage( BufferedImage image, String path, String filename, float quality ) throws IOException
	{
		final File file = new File( path + filename );
		writeImage( image, file, quality );
	}

	/**
	 * Grava a imagem em disco no caminho informado, com o nome informado. Esse método não modifica
	 * a imagem original.
	 * 
	 * @param image Imagem
	 * @param path Caminho
	 * @param filename Nome do arquivo
	 * @return Arquivo contendo os dados da imagem
	 * @throws IOException Lançada quando não for possível gravar no local informado.
	 */
	public static File writeImage( BufferedImage image, String path, String filename ) throws IOException
	{
		final File dir = new File( path );
		dir.mkdirs();

		final File file = new File( path + filename );
		file.createNewFile();

		writeImage( image, file, 1 );

		return file;
	}
}
